using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Collections;
using System.Globalization;
using System.Diagnostics;

namespace OsdmSoftware.CommonLibrary
{
    public class ParserException: ApplicationException
    {
        public ParserException(string message) : base(message) { }
    }

    public class ScriptContext
    {
        Dictionary<string, object> constants = new Dictionary<string, object>();
        Dictionary<string, object> variables;
        //List<Assembly> referencedAssemblies = new List<Assembly>();
        Dictionary<string, Assembly> namespaces = new Dictionary<string, Assembly>();

        public Dictionary<string, object> Constants { get { return constants; } }
        public Dictionary<string, object> Variables { get { return variables; } }
        //public List<Assembly> ReferencedAssemblies { get { return referencedAssemblies; } }
        public Dictionary<string, Assembly> Namespaces { get { return namespaces; } }

        public ScriptContext(Dictionary<string, object> variables, string[] constantNames, object[] constantValues, string[] namespaces)
        {
            this.variables = variables;
            if (variables == null)
                this.variables = new Dictionary<string, object>();

            Trace.Assert(constantNames.Length == constantValues.Length);
            for (int i = 0; i < constantNames.Length; i++)
            {
                constants.Add(constantNames[i], constantValues[i]);
            }

            foreach (string ns in namespaces)
            {
                this.namespaces.Add(ns, null);
            }
        }
    }

    public class ScriptParser
    {
        static SetOfChar IdentChars = new SetOfChar();
        static SetOfChar Digits = new SetOfChar();
        static SetOfChar WhiteSpaces = new SetOfChar();
        static SetOfChar DoubleSymbols = new SetOfChar();

        static ScriptParser()
        {
            IdentChars.LatinLetters = true;
            IdentChars.Digits = true;
            IdentChars.OtherCharacters = ":";
            Digits.Digits = true;
            WhiteSpaces.WhiteSpaces = true;
            DoubleSymbols.OtherCharacters = "<>=!&|";
            IdentChars.InitSet();
            Digits.InitSet();
            WhiteSpaces.InitSet();
            DoubleSymbols.InitSet();
        }

        public abstract class AssignmentInfo
        {
            public abstract void Assign(object value);
        }

        public class PropertyAssignmentInfo: AssignmentInfo
        {
            public object obj;
            public PropertyInfo property;
            public object[] parameters;
            public PropertyAssignmentInfo(object obj, PropertyInfo property, object[] parameters)
            {
                this.obj = obj;
                this.property = property;
                this.parameters = parameters;
            }

            public override void Assign(object value)
            {
                property.SetValue(obj, value, parameters);
            }
        }

        public class FieldAssignmentInfo: AssignmentInfo
        {
            public object obj;
            public FieldInfo field;
            public FieldAssignmentInfo(object obj, FieldInfo field)
            {
                this.obj = obj;
                this.field = field;
            }

            public override void Assign(object value)
            {
                field.SetValue(obj, value);
            }
        }

        public class VariableAssignmentInfo: AssignmentInfo
        {
            public Dictionary<string, object> list;
            public string name;

            public VariableAssignmentInfo(Dictionary<string, object> list, string name)
            {
                this.list = list;
                this.name = name;
            }

            public override void Assign(object value)
            {
                list[name] = value;
            }
        }

        enum LexemType { Identifier, Number, QuotedString, Symbol, EOF }

        PChar pos;

        LexemType lexemType = LexemType.EOF;
        string lexem = "";
        Dictionary<string, object> localVariables = new Dictionary<string, object>();
        ScriptContext scriptContext;

        public PChar Pos { get { return pos; } }

        public ScriptParser(PChar pos, ScriptContext scriptContext)
        {
            this.pos = pos;
            this.scriptContext = scriptContext;
        }

        void NextLexem()
        {
            pos.SkipSet(pos, WhiteSpaces);
            if (Digits.Contains(pos.Current))
            {
                lexem = pos.ParseNumber(pos, '.');
                lexemType = LexemType.Number;
            }
            else if (pos.Current == '"')
            {
                pos.Move();
                lexem = pos.ParseQuotedString(pos, '"');
                lexemType = LexemType.QuotedString;
            }
            else if (pos.Current == ':')
            {
                lexem = new String(pos.Current, 1);
                lexemType = LexemType.Symbol;
                pos.Move();
            }
            else if (IdentChars.Contains(pos.Current))
            {
                PChar startPos = pos;
                pos.SkipSet(startPos, IdentChars);
                lexem = startPos.GetBeginning(pos);
                lexemType = LexemType.Identifier;
            }
            else if (DoubleSymbols.Contains(pos.Current))
            {
                PChar startPos = pos;
                pos.SkipSet(startPos, DoubleSymbols);
                lexem = startPos.GetBeginning(pos);
                lexemType = LexemType.Symbol;
            }
            else if (pos.Current == Char.MinValue)
            {
                lexemType = LexemType.EOF;
                lexem = "";
            }
            else
            {
                lexem = new String(pos.Current, 1);
                lexemType = LexemType.Symbol;
                pos.Move();
            }
        }

        ArrayList GetCommaList(string endSymbol)
        {
            ArrayList list = new ArrayList();
            while (lexemType != LexemType.Symbol || !lexem.Equals(endSymbol))
            {
                object result = ComputeExpression();
                list.Add(result);
                if (lexemType == LexemType.Symbol && lexem.Equals(","))
                {
                    NextLexem();
                }
                else if (lexemType == LexemType.Symbol && lexem.Equals(endSymbol))
                {
                }
                else
                {
                    throw new ParserException("',' or '" + endSymbol + "' expected");
                }
            }

            NextLexem();
            return list;
        }

        Type TryLookupType(string typeName)
        {
            string dottedTypeName = typeName.Replace("::", ".");
            Type result;
            if (dottedTypeName.IndexOf('.') != -1)
            {
                return TryLookupFullyQualifiedType(dottedTypeName);
            }
            else
            {
                foreach (KeyValuePair<string, Assembly> ns in scriptContext.Namespaces)
                {
                    if (ns.Value == null)
                    {
                        result = TryLookupFullyQualifiedType(ns.Key + "." + dottedTypeName);
                    }
                    else
                    {
                        result = ns.Value.GetType(ns.Key + "." + dottedTypeName);
                    }

                    if (result != null)
                        return result;
                }
            }

            return null;
        }

        Type TryLookupFullyQualifiedType(string dottedTypeName)
        {
            Type result = Type.GetType(dottedTypeName);

            // Fully qualified name
            /*foreach (Assembly ass in scriptContext.ReferencedAssemblies)
            {
                result = ass.GetType(dottedTypeName);
                if (result != null)
                    return result;
            }*/

            return result;
        }

        object ComputeElementTerm()
        {
            object result;
            if (lexemType == LexemType.Identifier)
            {
                if (lexem == "new")
                {
                    NextLexem();
                    if (lexemType != LexemType.Identifier)
                    {
                        throw new ParserException("Identifier expected after new");
                    }

                    Type type = TryLookupType(lexem);
                    NextLexem();

                    Type[] paramTypes = null;
                    if (lexemType == LexemType.Symbol && lexem.Equals("%"))
                    {
                        NextLexem();
                        paramTypes = (Type[]) GetCommaList("%").ToArray(typeof(Type));
                    }

                    object[] parameters = new object[0];
                    if (lexemType == LexemType.Symbol && lexem.Equals("("))
                    {
                        NextLexem();
                        parameters = GetCommaList(")").ToArray();
                    }

                    ConstructorInfo ci = null;
                    if (paramTypes != null)
                    {
                        if (paramTypes.Length != parameters.Length)
                        {
                            throw new ParserException("Number of formal and fact parameters must match");
                        }

                        ci = type.GetConstructor(paramTypes);
                        if (ci == null)
                        {
                            throw new ParserException("No constructor in type " + type.FullName + " with such parameter types");
                        }
                    }
                    else
                    {
                        ConstructorInfo[] cia = type.GetConstructors();
                        foreach (ConstructorInfo cic in cia)
                        {
                            ParameterInfo[] pia = cic.GetParameters();
                            if (pia.Length == parameters.Length)
                            {
                                if (ci != null)
                                {
                                    throw new ParserException("More than one constructor with " + parameters.Length.ToString() + " parameter(s) in type " + type.FullName + ". Specify desired param type after type name with %, such as new System::Collections::ArrayList%System::Int32%(5)");
                                }

                                ci = cic;
                            }
                        }   
                    }

                    result = ci.Invoke(parameters);

                    return result;
                }
                else if (lexem == "true")
                {
                    NextLexem();
                    return true;
                }
                else if (lexem == "false")
                {
                    NextLexem();
                    return false;
                }
                else if (lexem == "null")
                {
                    NextLexem();
                    return null;
                }
                else
                {
                    // First try types
                    Type type = TryLookupType(lexem);
                    if (type != null)
                    {
                        NextLexem();
                        return type;
                    }
                    
                    // Next try local variables
                    if (localVariables.TryGetValue(lexem, out result))
                    {
                        string varName = lexem;
                        NextLexem();
                        if (lexemType == LexemType.Symbol && lexem.Equals("="))
                            return new VariableAssignmentInfo(localVariables, varName);
                        else
                            return result;
                    }

                    // Now constants
                    if (scriptContext.Constants.TryGetValue(lexem, out result))
                    {
                        NextLexem();
                        return result;
                    }

                    // Global variables
                    if (scriptContext.Variables.TryGetValue(lexem, out result))
                    {
                        string varName = lexem;
                        NextLexem();
                        if (lexemType == LexemType.Symbol && lexem.Equals("="))
                            return new VariableAssignmentInfo(scriptContext.Variables, varName);
                        else
                            return result;
                    }

                    throw new ParserException("Unknown identifier " + lexem);
                }
            }
            else if (lexemType == LexemType.Symbol && lexem.Equals("("))
            {
                NextLexem();
                result = ComputeExpression();
                if (lexemType != LexemType.Symbol || !lexem.Equals(")"))
                {
                    throw new ParserException("')' expected");
                }
                NextLexem();
                return result;
            }
            else if (lexemType == LexemType.Number)
            {
                if (lexem.IndexOfAny(new char[] { '.', 'E', 'e' }) == -1)
                    result = Int32.Parse(lexem);
                else
                    result = Double.Parse(lexem, CultureInfo.InvariantCulture.NumberFormat);

                NextLexem();
                return result;
            }
            else if (lexemType == LexemType.QuotedString)
            {
                result = lexem;

                NextLexem();
                return result;
            }
            else if (lexemType == LexemType.EOF)
            {
                throw new ParserException("Unexpected end of file");
            }

            throw new ParserException("What is this? lexemType = " + lexemType.ToString() + " lexem = '" + lexem + "'");
        }

        object ProcessMethodOrProperty(Type type, object obj, string name, BindingFlags bf)
        {
            MethodInfo method = null;
            PropertyInfo property = null;
            FieldInfo field = null;
            NextLexem();

            Type[] paramTypes = null;
            if (lexemType == LexemType.Symbol && lexem.Equals("%"))
            {
                NextLexem();
                paramTypes = (Type[]) GetCommaList("%").ToArray(typeof(Type));
                method = type.GetMethod(name, paramTypes);
            }
            else
            {
                MemberInfo[] mia = type.GetMember(name, bf);
                foreach (MemberInfo mi in mia)
                {
                    if (mi is PropertyInfo)
                        property = (PropertyInfo) mi;
                    else if (mi is MethodInfo)
                        method = (MethodInfo) mi;
                    else if (mi is FieldInfo)
                        field = (FieldInfo) mi;
                }
            }

            if (method != null)
            {
                if (lexemType == LexemType.Symbol && lexem.Equals("("))
                {
                    NextLexem();
                    ArrayList list = GetCommaList(")");
                    return method.Invoke(obj, list.ToArray());                    
                }
                else
                {
                     throw new ParserException(method.Name + " is a method and must be followed by '('");
                }
            }
            else if (property != null)
            {
                object[] parameters;
                ParameterInfo[] indexParams = property.GetIndexParameters();
                if (indexParams.Length != 0)
                {
                    if (lexemType == LexemType.Symbol && lexem.Equals("["))
                    {
                        NextLexem();
                        parameters = GetCommaList("]").ToArray();
                    }
                    else
                    {
                        throw new ParserException(property.Name + " is an indexed property and must be followed by '['");
                    }
                }
                else
                    parameters = null;

                if (lexemType == LexemType.Symbol && lexem.Equals("="))
                {
                    return new PropertyAssignmentInfo(obj, property, parameters);
                }

                return property.GetValue(obj, parameters);
            }
            else if (field != null)
            {
                if (lexemType == LexemType.Symbol && lexem.Equals("="))
                {
                    return new FieldAssignmentInfo(obj, field);
                }

                return field.GetValue(obj);
            }
            else// if (method == null && property == null)
            {
                throw new ParserException("There is no " + (obj == null ? "static" : "instance") + " method or property named " + lexem + " in class " + type.FullName);
            }
        }

        object ComputeTerm()
        {
            object result = ComputeElementTerm();
            while (lexemType == LexemType.Symbol && lexem.Equals("."))
            {
                NextLexem();
                if (lexemType != LexemType.Identifier)
                {
                    throw new ParserException("Identifier must follow '.'");
                }

                if (result == null)
                {
                    throw new ParserException("Value is null - cannot call method or property");
                }


                if (result is Type)
                {
                    result = ProcessMethodOrProperty((Type) result, null, lexem, BindingFlags.Static | BindingFlags.Public);
                }
                else
                {
                    result = ProcessMethodOrProperty(result.GetType(), result, lexem, BindingFlags.Instance | BindingFlags.Public);
                }
            }

            return result;
        }

        object ComputeMulDiv()
        {
            object result = ComputeTerm();
            while (lexemType == LexemType.Symbol && (lexem.Equals("*") || lexem.Equals("/")))
            {
                bool div = lexem.Equals("/");
                NextLexem();
                object result1 = ComputeTerm();
                if (div)
                    result = (int) result / (int) result1;
                else
                    result = (int) result * (int) result1;
            }

            return result;
        }

        object ComputePlusMinus()
        {
            bool minus = false;
            if (lexemType == LexemType.Symbol && lexem.Equals("-"))
            {
                minus = true;
                NextLexem();
            }

            object result = ComputeMulDiv();
            if (minus)
            {
                result = -(int) result;
            }

            if (result is string)
            {
                while (lexemType == LexemType.Symbol && lexem.Equals("+"))
                {
                    NextLexem();
                    object result1 = ComputeMulDiv();
                    result = (string) result + (string) result1;
                }
            }
            else
            {
                while (lexemType == LexemType.Symbol && (lexem.Equals("+") || lexem.Equals("-")))
                {
                    minus = lexem.Equals("-");
                    NextLexem();
                    object result1 = ComputeMulDiv();
                    if (minus)
                        result = (int) result - (int) result1;
                    else
                        result = (int) result + (int) result1;
                }
            }

            return result;
        }

        object ComputeComparison()
        {
            object result = ComputePlusMinus();
            if (lexemType == LexemType.Symbol && lexem.Equals("=="))
            {
                NextLexem();
                object result1 = ComputePlusMinus();
                result = result.Equals(result1);
            }
            else if (lexemType == LexemType.Symbol && lexem.Equals("!="))
            {
                NextLexem();
                object result1 = ComputePlusMinus();
                result = !result.Equals(result1);
            }
            else if (lexemType == LexemType.Symbol && lexem.Equals(">"))
            {
                NextLexem();
                object result1 = ComputePlusMinus();
                result = (int) result > (int) result1;
            }
            else if (lexemType == LexemType.Symbol && lexem.Equals("<"))
            {
                NextLexem();
                object result1 = ComputePlusMinus();
                result = (int) result < (int) result1;
            }
            else if (lexemType == LexemType.Symbol && lexem.Equals("<="))
            {
                NextLexem();
                object result1 = ComputePlusMinus();
                result = (int) result <= (int) result1;
            }
            else if (lexemType == LexemType.Symbol && lexem.Equals(">="))
            {
                NextLexem();
                object result1 = ComputePlusMinus();
                result = (int) result >= (int) result1;
            }

            return result;
        }

        object ComputeNot()
        {
            bool not = false;
            if (lexemType == LexemType.Symbol && lexem.Equals("!"))
            {
                not = true;
                NextLexem();
            }

            object result = ComputeComparison();
            if (not)
                result = !(bool) result;

            return result;
        }

        object ComputeAnd()
        {
            object result = ComputeNot();
            while (lexemType == LexemType.Symbol && lexem.Equals("&&"))
            {
                NextLexem();
                object result1 = ComputeNot();
                result = (bool) result && (bool) result1;
            }

            return result;
        }

        object ComputeOr()
        {
            object result = ComputeAnd();
            while (lexemType == LexemType.Symbol && lexem.Equals("||"))
            {
                NextLexem();
                object result1 = ComputeAnd();
                result = (bool) result || (bool) result1;
            }

            return result;
        }

        object ComputeExpression()
        {
            return ComputeOr();
        }

        void SkipExpression()
        {
            if (lexemType != LexemType.Symbol || !lexem.Equals("("))
            {
                throw new ParserException("'(' expected");
            }

            int level = 1;
            NextLexem();

            while (level > 0)
            {
                if (lexemType == LexemType.Symbol && lexem.Equals("("))
                {
                    level++;
                }
                else if (lexemType == LexemType.Symbol && lexem.Equals(")"))
                {
                    level--;
                }
                else if (lexemType == LexemType.EOF)
                {
                    throw new ParserException("')' expected (" + level.ToString() + " times");
                }

                NextLexem();
            }
        }

        void SkipBlock()
        {
            if (lexemType != LexemType.Symbol || !lexem.Equals("{"))
            {
                throw new ParserException("'{' expected");
            }

            int level = 1;
            NextLexem();

            while (level > 0)
            {
                if (lexemType == LexemType.Symbol && lexem.Equals("{"))
                {
                    level++;
                }
                else if (lexemType == LexemType.Symbol && lexem.Equals("}"))
                {
                    level--;
                }
                else if (lexemType == LexemType.EOF)
                {
                    throw new ParserException("'}' expected (" + level.ToString() + " times");
                }

                NextLexem();
            }
        }

        void ProcessBlock(bool processAsGlobal)
        {
            if (lexemType != LexemType.Symbol || !lexem.Equals("{"))
            {
                throw new ParserException("'{' expected");
            }

            NextLexem();

            while (lexemType != LexemType.Symbol || !lexem.Equals("}"))
            {
                bool processed = true;
                if (lexemType == LexemType.Identifier)
                {
                    if (lexem.Equals("var"))
                    {
                        NextLexem();

                        if (lexemType != LexemType.Identifier)
                        {
                            throw new ParserException("Identifier (variable name) expected after 'var'");
                        }

                        string varName = lexem;

                        Dictionary<string, object> vars;
                        if (processAsGlobal)
                            vars = scriptContext.Variables;
                        else
                            vars = localVariables;

                        /*if (vars.ContainsKey(varName))
                        {
                            throw new ParserException("Variable " + varName + " already exists");
                        }*/

                        NextLexem();
                        if (lexemType == LexemType.Symbol && lexem.Equals("="))
                        {
                            NextLexem();
                            object value = ComputeExpression();
                            vars[varName] = value;
                        }
                        else if (!vars.ContainsKey(varName))
                            vars[varName] = null;

                        if (lexemType != LexemType.Symbol || !lexem.Equals(";"))
                        {
                            throw new ParserException("';' expected");
                        }

                        NextLexem();
                    }
                    else if (lexem.Equals("if"))
                    {
                        NextLexem();
                        object condition = ComputeExpression();
                        bool res = (bool) condition;
                        if (res)
                        {
                            ProcessBlock(processAsGlobal);
                        }
                        else
                        {
                            SkipBlock();
                        }

                        while (lexemType == LexemType.Identifier && lexem.Equals("else"))
                        {
                            NextLexem();
                            if (lexemType == LexemType.Identifier && lexem.Equals("if"))
                            {
                                NextLexem();
                                bool res1 = false;
                                if (res)
                                {
                                    SkipExpression();
                                    SkipBlock();
                                }
                                else
                                {
                                    condition = ComputeExpression();
                                    res1 = (bool) condition;
                                    if (res1)
                                    {
                                        ProcessBlock(processAsGlobal);
                                    }
                                    else
                                    {
                                        SkipBlock();
                                    }
                                    res = res1;
                                }
                            }
                            else
                            {
                                if (res)
                                {
                                    SkipBlock();
                                }
                                else
                                {
                                    ProcessBlock(processAsGlobal);
                                }

                                break;
                            }
                        }
                    }
                    else if (lexem.Equals("while"))
                    {
                        PChar pos1 = pos;
                        NextLexem();
                        while (true)
                        {
                            object condition = ComputeExpression();
                            bool res = (bool) condition;
                            if (!res)
                                break;

                            ProcessBlock(processAsGlobal);
                            pos = pos1;
                            NextLexem();
                        }

                        SkipBlock();
                    }
                    else if (lexem.Equals("foreach"))
                    {
                        throw new ParserException("foreach not implemented yet, use while");
                    }
                    else if (lexem.Equals("for"))
                    {
                        throw new ParserException("for not implemented yet, use while");
                    }
                    else
                    {
                        processed = false;
                    }
                }
                else
                {
                    if (lexemType == LexemType.Symbol && lexem.Equals("}"))
                        break;

                    processed = false;
                }

                if (!processed)
                {
                    object obj = ComputeExpression();
                    if (lexemType == LexemType.Symbol && lexem.Equals("="))
                    {
                        if (!(obj is AssignmentInfo))
                        {
                            throw new ParserException("Left side cannot be assigned to");
                        }

                        NextLexem();
                        object value = ComputeExpression();

                        ((AssignmentInfo) obj).Assign(value);
                    }

                    if (lexemType != LexemType.Symbol || !lexem.Equals(";"))
                    {
                        throw new ParserException("';' expected");
                    }

                    NextLexem();
                }
            }

            NextLexem();
        }

        public object Process(PChar pos, bool checkForEOF, bool block, bool processAsGlobal)
        {
            this.pos = pos;
            NextLexem();
            localVariables.Clear();

            object result = null;
            try
            {
                if (block)
                {
                    ProcessBlock(processAsGlobal);
                }
                else
                {
                    result = ComputeExpression();
                }

                if (checkForEOF)
                {
                    if (lexemType != LexemType.EOF)
                    {
                        throw new ParserException("end of file expected");
                    }
                }
            }
            catch (ParserException pe)
            {
                throw new ParserException(pe.Message + " when executing '" + this.pos.SourceText + "' at " + this.pos.GetLineColumn().ToString());
            }
            catch (Exception e)
            {
                throw new ParserException(e.GetType().FullName + " " + e.Message + " when executing '" + this.pos.SourceText + "' at " + this.pos.GetLineColumn().ToString());
            }

            return result;
        }
    }
}
